package org.geogebra.desktop.sound;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.LineEvent;
import javax.sound.sampled.LineListener;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;

import org.geogebra.common.kernel.geos.GeoFunction;
import org.geogebra.common.sound.FunctionSound;
import org.geogebra.common.util.debug.Log;

/**
 * Class for playing function-generated sounds.
 * 
 * @author G. Sturr
 *
 */
public final class FunctionSoundD extends FunctionSound
		implements LineListener {

	// threaded class to play function
	private SoundThread soundThread;

	// streaming audio fields
	private AudioFormat af;
	private SourceDataLine sdl;

	/**
	 * Constructs instance of FunctionSound
	 * 
	 * @throws Exception when initialization fails
	 */
	public FunctionSoundD() throws Exception {
		super();

		if (!initStreamingAudio(getSampleRate(), getBitDepth())) {
			throw new Exception("Cannot initialize streaming audio");
		}
	}

	/**
	 * Initializes instances of AudioFormat and SourceDataLine
	 * 
	 * @param sampleRate
	 *            = 8000, 16000, 11025, 16000, 22050, or 44100
	 * @param bitDepth
	 *            = 8 or 16
	 * @return
	 */
	@Override
	protected boolean initStreamingAudio(int sampleRate, int bitDepth) {
		if (!super.initStreamingAudio(sampleRate, bitDepth)) {
			return false;
		}

		boolean success = true;

		af = new AudioFormat(sampleRate, bitDepth, 1, true, true);
		try {
			sdl = AudioSystem.getSourceDataLine(af);
			// add listener when debugging
			// sdl.addLineListener(this);
		} catch (LineUnavailableException e) {
			e.printStackTrace();
			success = false;
		}
		return success;
	}

	/**
	 * Plays a sound generated by the time valued GeoFunction f(t), from t = min
	 * to t = max in seconds. The function is assumed to have range [-1,1] and
	 * will be clipped to this range otherwise.
	 * 
	 * @param geoFunction function
	 * @param min min x value
	 * @param max max x value
	 * @param sampleRate rate
	 * @param bitDepth depth
	 */

	@Override
	public void playFunction(final GeoFunction geoFunction, final double min,
			final double max, final int sampleRate, final int bitDepth) {
		if (!checkFunction(geoFunction, min, max, sampleRate, bitDepth)) {
			return;
		}

		// close current sound thread and prepare sdl
		if (soundThread != null) {
			soundThread.interrupt();
			sdl.flush();
			sdl.close();
		}

		// spawn a new SoundThread to play the function sound
		soundThread = new SoundThread();
		soundThread.start();

	}

	/**
	 * Pauses/resumes sound generation
	 * 
	 * @param doPause whether to pause
	 */
	@Override
	public void pause(boolean doPause) {

		if (doPause) {
			setMin(getT());
			soundThread.stopSound();
		} else {
			playFunction(getF(), getMin(), getMax(), getSampleRate(),
					getBitDepth());
		}
	}

	/**
	 * Listener for line events, used for debugging.
	 */
	@Override
	public void update(LineEvent le) {

		LineEvent.Type type = le.getType();

		if (type == LineEvent.Type.OPEN) {
			Log.debug("OPEN");
		} else if (type == LineEvent.Type.CLOSE) {
			Log.debug("CLOSE");
		} else if (type == LineEvent.Type.START) {
			Log.debug("START");
		} else if (type == LineEvent.Type.STOP) {
			Log.debug("STOP");
		}
	}

	/**********************************************************
	 * Class SoundThread
	 * 
	 * Plays sounds from time-valued functions.
	 *********************************************************/
	private class SoundThread extends Thread {

		private volatile boolean stopped = false;

		protected SoundThread() {
		}

		@Override
		public void run() {
			generateFunctionSound();
		}

		private void generateFunctionSound() {

			stopped = false;

			// time between samples
			setSamplePeriod(1.0 / getSampleRate());

			// create internal buffer for mathematically generated sound data
			// a small buffer minimizes latency when the function changes
			// dynamically
			// TODO: find optimal buffer size
			int frameSetSize = getSampleRate() / 50; // 20ms ok?
			if (getBitDepth() == 8) {
				setBuf(new byte[frameSetSize]);
			} else {
				setBuf(new byte[2 * frameSetSize]);
			}

			// generate the function sound
			try {

				// open the sourceDataLine
				// TODO: the sdl buffer size is relative to our internal buffer
				// need to experiment for best sizing factor
				sdl.open(af, 10 * getBufLength());
				sdl.start();

				if (getBitDepth() == 16) {
					setT(getMin());
					loadBuffer16(getT());
					doFade(getBuf()[0], false);
					sdl.write(getBuf(), 0, getBufLength());
					do {
						setT(getT() + getSamplePeriod() * frameSetSize);
						loadBuffer16(getT());
						sdl.write(getBuf(), 0, getBufLength());
					} while (getT() < getMax() && !stopped);

					doFade(getBuf()[getBufLength() - 1], true);

				} else {
					// use 8-bit samples
					setT(getMin());
					loadBuffer8(getT());
					doFade(getBuf()[0], false);
					sdl.write(getBuf(), 0, getBufLength());
					do {
						setT(getT() + getSamplePeriod() * frameSetSize);
						loadBuffer8(getT());
						sdl.write(getBuf(), 0, getBufLength());
					} while (getT() < getMax() && !stopped);

					if (!stopped) {
						doFade(getBuf()[getBufLength() - 1], true);
					}

				}

				// finish transfer of bytes from internal buffer to the sdl
				// buffer
				sdl.drain();

				// stop and close the sourceDataLine
				sdl.stop();
				sdl.close();

			} catch (LineUnavailableException e) {
				e.printStackTrace();
			}

		}

		/**
		 * Shapes ends of waveform to fade sound data TODO: is this actually
		 * working?
		 * 
		 * @param peakValue pake value
		 * @param isFadeOut whether it's fading out
		 */
		private void doFade(short peakValue, boolean isFadeOut) {
			byte[] fadeBuf = getFadeBuffer(peakValue, isFadeOut);
			sdl.write(fadeBuf, 0, fadeBuf.length);
		}

		/**
		 * Stops function sound
		 */
		public void stopSound() {
			stopped = true;
		}
	}

}
